﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using PSE.Framework.Process;
using PSE.Hosting.ServiceHoster.Configuration;
using System.IO;
using System.ServiceProcess;
using PSE.Hosting.ServiceHoster.Logging;
using System.Configuration;

namespace PSE.Hosting.ServiceHoster.Business.Processes
{
    public class ServiceHosterProcessManager : HierarchicalProcessManager<HierarchicalProcess,
                                                                          ServiceHosterPackageProcess,
                                                                          IProcessService,
                                                                          ServiceHosterProcessManager>
    {
        #region Fields

        private const string WindowsServiceName = "PSEServiceHoster";

        #endregion

        #region HierarchicalProcessManager
        protected override Uri GetServiceAddress()
        {
            return new Uri("net.pipe://localhost/ServiceHosterService");
        }

        protected override IProcessService CreateServiceInstance()
        {
            return new ProcessService();
        }
        #endregion

        #region Windows Service Methods
        /// <summary>
        /// Stop the WCF Service Hoster windows service.
        /// </summary>
        public void StopWindowsService()
        {
            ServiceController controller = new ServiceController(WindowsServiceName);
            controller.Stop();
        }

        /// <summary>
        /// Start the WCF Service Hoster windows service.
        /// </summary>
        public void StartWindowsService(bool asynchronous, bool startAll)
        {
            ServiceController controller = new ServiceController(WindowsServiceName);
            controller.Start(new string[] { asynchronous.ToString(), startAll.ToString() });
        }
        #endregion

        #region Public Members

        /// <summary>
        /// Restart all the services contained in the assemblies of a single package.
        /// </summary>
        /// <param name="packageName">The name of the package to be refreshed.</param>
        /// <returns>A list of failure or success messages.</returns>
        public List<MessageData> RefreshPackageAssemblies(string packageName)
        {
            // Stop all assemblies from the package that had a assembly changed.
            this.StopAll(packageName);
            // Start all assemblies from the package that had a assembly changed.
            List<MessageData> messages = this.StartAll(packageName);

            return messages;
        }

        /// <summary>
        /// Starts all assemblies of all packages for this machine.
        /// </summary>
        public List<MessageData> StartAll()
        {
            List<MessageData> messagesList = new List<MessageData>();

            List<PackageElement> packages = ConfigurationFile.Instance.GetPackages();

            foreach (PackageElement package in packages)
            {
                if (package.IsStarted)
                {
                    try
                    {
                        List<MessageData> messages = this.StartAll(package.Name);
                        messagesList.AddRange(messages);
                    }
                    catch (Exception ex)
                    {
                        messagesList.Add(new MessageData(MessageData.MessageTypes.Error, ex.ToString()));
                    }
                }
            }

            return messagesList;
        }

        /// <summary>
        /// Start all services a specified package.
        /// </summary>
        /// <param name="packageName">The package name to start the services.</param>
        public List<MessageData> StartAll(string packageName)
        {
            List<MessageData> messagesList = new List<MessageData>();

            messagesList = this.GetPackage(packageName).StartAll(true);

            ConfigurationFile.Instance.SavePackageStarted(packageName);

            return messagesList;
        }

        /// <summary>
        /// Stop Package
        /// </summary>
        /// <param name="packageName">Package Name</param>
        public void StopPackage(string packageName)
        {
            StopAll(packageName, false);
        }

        /// <summary>
        /// Start Package
        /// </summary>
        /// <param name="packageName">Package Name</param>
        public void StartPackage(string packageName)
        {
            this.GetPackage(packageName).StartAll(false);

            ConfigurationFile.Instance.SavePackageStarted(packageName);
        }

        /// <summary>
        /// Stop all the services from an assembly.
        /// </summary>
        /// <param name="packageName">The name of the package containing the assembly to be stopped.</param>
        /// <param name="codeBase">The name of the assembly to be stopped.</param>
        public void StopAssembly(string packageName, string codeBase)
        {
            this.GetPackage(packageName).StopAssembly(codeBase);
        }

        /// <summary>
        /// Start all WCF services from a specific assembly.
        /// </summary>
        /// <param name="packageName">The name of the package of the assembly to start.</param>
        /// <param name="fileName">The name of the assembly file to start. This is the filename only, without the path.</param>
        public List<MessageData> StartAssembly(string packageName, string fileName)
        {
            return this.GetPackage(packageName).StartAssembly(fileName);
        }

        /// <summary>
        /// Starts a Service configured within the Hoster app.config.
        /// </summary>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to start.</param>
        public void StartService(string packageName, string assembly, string serviceName)
        {
            this.GetPackage(packageName).StartService(assembly, serviceName);
        }

        /// <summary>
        /// Stops the specified service.
        /// </summary>
        /// <param name="packageName">The name of the package containing the service.</param>
        /// <param name="assembly">The assembly name.</param>
        /// <param name="serviceName">The service name to stop.</param>
        public void StopService(string packageName, string assembly, string serviceName)
        {
            this.GetPackage(packageName).StopService(assembly, serviceName);
        }

        /// <summary>
        /// Stops all the services already started.
        /// </summary>
        public void StopAll()
        {
            for (int i = this.Children.Count - 1; i >= 0; i--)
            {
                this.StopAll(this.Children[i].Name, false);
            }
        }

        /// <summary>
        /// Stops all the services already started from a specific package.
        /// </summary>
        public void StopAll(string packageName)
        {
            this.StopAll(packageName, true);
        }

        private void StopAll(string packageName, bool save)
        {
            if (save)
                this.VerifyPackageExists(packageName);

            if (this.IsChildStarted(packageName))
            {
                this.Children[packageName].StopAll();
                this.CloseChild(packageName);
            }

            if (save)
                ConfigurationFile.Instance.SavePackageStoped(packageName);
        }

        /// <summary>
        /// Gets a list of all the services configured within the Service Hoster app.config.
        /// </summary>
        /// <returns></returns>
        public List<Entities.Service> GetServiceList(string packageName, string assembly)
        {
            return this.GetPackage(packageName).GetServiceList(assembly);
        }

        /// <summary>
        /// Gets a list of all packages created on this server.
        /// </summary>
        /// <returns>List of packages.</returns>
        public List<Entities.Package> GetPackageList()
        {
            List<Entities.Package> list = new List<Entities.Package>();

            List<PackageElement> packages = ConfigurationFile.Instance.GetPackages();

            foreach (PackageElement p in packages)
            {
                int processId = this.Children[p.Name] != null ? this.Children[p.Name].ProcessId : -1;

                Entities.Package package = new Entities.Package(processId, p.IsStarted, p.Name, p.Domain, p.UserName);
                list.Add(package);
            }

            return list;
        }

        /// <summary>
        /// Returns a list of all the assemblies added in a package.
        /// </summary>
        /// <param name="packageName">The name of the package to get the list of assenblies.</param>
        /// <returns>List of added assemblies.</returns>
        public List<Entities.Assembly> GetAssemblyList(string packageName)
        {
            return this.GetPackage(packageName).GetAssemblyList();
        }

        /// <summary>
        /// Adds a new package to the Service Hoster.
        /// </summary>
        /// <param name="packageName">The name of the package to be added.</param>
        public void AddPackage(string packageName, string domain, string userName, string password)
        {
            packageName = packageName.ToUpper();

            if (ConfigurationFile.Instance.GetPackages().Any(p => p.Name == packageName))
            {
                throw new Business.PackageAlreadyExistsException(string.Format("The '{0}' package already exists", packageName));
            }

            this.ValidateLogin(ref domain, ref userName, ref password);

            // Adds the new package to the configuration file.
            try
            {
                ConfigurationFile.Instance.AddPackage(packageName, domain, userName, password);
            }
            catch (Configuration.PackageAlreadyExistsException)
            {
                throw new Business.PackageAlreadyExistsException(string.Format("The '{0}' package already exists", packageName));
            }
            catch (Exception ex)
            {
                Logger.Log(ex, Logger.EntryType.Error);
                throw new Exception("Problems adding the package.", ex);
            }

            Logger.Log(string.Format("Package '{0}' was added.", packageName), Logger.EntryType.Information);
        }

        /// <summary>
        /// Update a package in the Service Hoster.
        /// </summary>
        /// <param name="packageName">The name of the package to be added.</param>
        public void UpdatePackage(string packageName, string domain, string userName, string password)
        {
            packageName = packageName.ToUpper();

            this.VerifyPackageExists(packageName);
            this.ValidateLogin(ref domain, ref userName, ref password);

            bool start = false;

            // Stops all assemblies from this package
            if (this.IsChildStarted(packageName))
            {
                this.StopAll(packageName);
                start = true;
            }

            // Update the package from the configuration.
            ConfigurationFile.Instance.UpdatePackage(packageName, domain, userName, password);

            // Start all assemblies from this package
            if (start)
                this.StartAll(packageName);

            Logger.Log(string.Format("Package '{0}' was updated.", packageName), Logger.EntryType.Information);
        }

        /// <summary>
        /// Removes a package from the Service Hoster.
        /// </summary>
        /// <param name="packageName"></param>
        public void RemovePackage(string packageName)
        {
            packageName = packageName.ToUpper();

            this.VerifyPackageExists(packageName);

            // Stops all assemblies from this package
            if (this.IsChildStarted(packageName))
                this.StopAll(packageName);

            // Removes the package from the configuration.
            ConfigurationFile.Instance.RemovePackage(packageName);

            Logger.Log(string.Format("Package '{0}' was removed.", packageName), Logger.EntryType.Information);
        }

        /// <summary>
        /// Adds a Service to the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="codeBase">The assembly code base.</param>
        /// <param name="domain">The domain name of the user.</param>
        /// <param name="userName">The user name for impersonate service operations.</param>
        /// <param name="password">The user password.</param>
        public void AddAssembly(string packageName, string codeBase)
        {
            this.GetPackage(packageName).AddAssembly(codeBase);
        }

        /// <summary>
        /// Removes a Service from the Service Hoster config file.
        /// </summary>
        /// <param name="packageName">The name of the package.</param>
        /// <param name="assemblyFile">The name of the assembly file (without path).</param>
        public void RemoveAssembly(string packageName, string assemblyFile)
        {
            this.GetPackage(packageName).RemoveAssembly(assemblyFile);
        }

        #endregion

        private void VerifyPackageExists(string packageName)
        {
            if (!ConfigurationFile.Instance.GetPackages().Any(p => p.Name == packageName))
                throw new InvalidPackageException(string.Format("The '{0}' package does not exists", packageName));
        }

        private void ValidateLogin(ref string domain, ref string userName, ref string password)
        {
            if (string.IsNullOrEmpty(domain) && !string.IsNullOrEmpty(userName))
                domain = System.Environment.MachineName;

            if (!string.IsNullOrEmpty(domain) && !string.IsNullOrEmpty(userName))
            {
                domain = domain.ToUpper();

                Impersonation imp = new Impersonation(domain, userName, password);
                imp.ValidateLogin();
            }
            else
            {
                domain = null;
                userName = null;
                password = null;
            }
        }

        private ServiceHosterPackageProcess GetPackage(string packageName)
        {
            ServiceHosterPackageProcess process = this.Children[packageName];

            if (process == null || !process.IsStarted)
            {
                PackageElement package = ConfigurationFile.Instance.GetPackages().FirstOrDefault(p => p.Name == packageName);

                if (package == null)
                    throw new InvalidPackageException(string.Format("The '{0}' package does not exist", packageName));

                process = this.StartChild(new ChildProcessStartInfo()
                    {
                        Name = packageName,
                        FileName = "ServiceHosterProcess.exe",
                        Domain = package.Domain,
                        UserName = package.UserName,
                        Password = package.Password,
                        CheckChildIsAlive = true,
                        CheckParentIsAlive = true
                    }
                );
            }

            return process;
        }
    }
}
